Skip to content

Conversation

@arttonoyan
Copy link
Contributor

This PR

Extract the minimal, provider-agnostic DI surface into a new package: OpenFeature.DependencyInjection.Abstractions.
This isolates the contracts and lightweight wiring needed to integrate any OpenFeature provider without pulling in a concrete implementation.

Related Issues

Fixes #587

Notes

  • Options split: Extracted provider-agnostic configuration from OpenFeatureOptions into a new base options type: OpenFeatureProviderOptions.
  • Inheritance: OpenFeatureOptions now inherits from OpenFeatureProviderOptions.
  • Internal wiring update: Internal configuration that previously targeted OpenFeatureOptions now targets OpenFeatureProviderOptions.

Before (internal)

services.AddOptions<OpenFeatureOptions>()
    .Configure(options =>
    {
        options.AddProviderName(null);
    });

After (internal)

services.AddOptions<OpenFeatureProviderOptions>()
    .Configure(options =>
    {
        options.AddProviderName(null);
    });

Note: This is an internal refactor. It does not affect consumers unless they were directly modifying OpenFeatureOptions via DI options configuration (which is not a supported/typical usage).


Impact

  • No new functionality.
  • Behavior unchanged at runtime (same provider selection, same evaluation outcomes, same logs).
  • Binary/source compatibility: Consumers using the documented extension methods and standard registration remain unaffected.
  • Potential touchpoints: Only internal code paths (or unconventional consumer code directly configuring OpenFeatureOptions) require updates to target OpenFeatureProviderOptions.

How to test

This is a regression-only refactor.

Expectations

  • All existing unit/integration tests pass unchanged.
  • No DI resolution or startup configuration differences.

Introduced a new project, `OpenFeature.DependencyInjection.Abstractions`, to the solution. This project supports dependency injection abstractions and targets multiple frameworks (`netstandard2.0`, `net8.0`, `net9.0`, `net462`) for broad compatibility.

Configured the project with the `Microsoft.NET.Sdk` SDK and set the root namespace to `OpenFeature.DependencyInjection.Abstractions`. Added dependencies on `Microsoft.Extensions.DependencyInjection.Abstractions` and `Microsoft.Extensions.Options` to enable DI and options configuration.
Refactored the OpenFeature framework to introduce `OpenFeatureProviderBuilder`, enhancing support for dependency injection and provider management.

- Changed namespaces to align with DI abstractions.
- Made `FeatureCodes` public for broader accessibility.
- Added `InternalsVisibleTo` for testing and project references.
- Introduced `OpenFeatureProviderBuilder` for managing providers and policies.
- Added extension methods for provider and policy registration.
- Refactored `OpenFeatureBuilder` to inherit from `OpenFeatureProviderBuilder`.
- Consolidated shared functionality in `OpenFeatureProviderOptions`.
- Updated `FeatureBuilderExtensions` and `InMemoryProviderOptions` to use the new abstraction.
- Updated tests to reflect new method signatures and hierarchy.
- Removed redundant methods and properties, improving code organization.

These changes improve maintainability, extensibility, and alignment with modern DI patterns.
Simplified the `AddProvider` API by removing the `TFeatureProvider` generic type parameter and directly using the `FeatureProvider` type. Updated the `TOptions` parameter to ensure it derives from `OpenFeatureProviderOptions`.

Added a `<ProjectReference>` to `OpenFeature.csproj` in `OpenFeature.DependencyInjection.Abstractions.csproj`. Updated `OpenFeatureOptions` to `OpenFeatureProviderOptions` in the default name selector policy.

Refactored `AddInMemoryProvider` methods to align with the new API. Updated tests in `OpenFeatureBuilderExtensionsTests` to reflect the changes and validate the updated functionality.

Performed general code cleanup, including XML documentation updates, to improve clarity and maintain consistency across the codebase.
Refactored `FeatureLifecycleManager` to modularize provider, hook, and handler initialization with new methods: `InitializeProvidersAsync`, `InitializeHooks`, and `InitializeHandlers`. Updated `EnsureInitializedAsync` to use these methods for improved readability and maintainability.

Revised `AddInMemoryProvider` in `FeatureBuilderExtensions` to use a generic `FeatureProvider` abstraction. Adjusted `CreateProvider` methods accordingly.

Improved code clarity in `FeatureFlagIntegrationTest` by renaming variables for consistency and removing redundant assignments.
Updated FeatureLifecycleManagerTests to replace OpenFeatureOptions
with OpenFeatureProviderOptions for configuring feature providers.
Added support for hooks and keyed singletons to enhance modularity.
Introduced additional feature flag retrieval in FeatureFlagIntegrationTest.
Added dependency on OpenFeature.DependencyInjection.Abstractions.
@codecov
Copy link

codecov bot commented Oct 4, 2025

Codecov Report

❌ Patch coverage is 96.85039% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.68%. Comparing base (29e1657) to head (f9a6781).

Files with missing lines Patch % Lines
...stractions/OpenFeatureProviderBuilderExtensions.cs 90.24% 0 Missing and 4 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #596      +/-   ##
==========================================
- Coverage   89.76%   89.68%   -0.09%     
==========================================
  Files          77       80       +3     
  Lines        3166     3179      +13     
  Branches      364      369       +5     
==========================================
+ Hits         2842     2851       +9     
  Misses        253      253              
- Partials       71       75       +4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

/// </code>
/// </example>
internal static class FeatureCodes
public static class FeatureCodes
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Do we need these FeatureCodes anymore?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kylejuliandev Answer: Yes - within the scope of this PR, FeatureCodes are still required; the current changes reference them.
Follow-up proposal: It’s a good time to graduate them from Experimental. Not in this PR - I suggest a separate task.
@askpt @beeme1mr what do you think about me opening a follow-up issue to:

  • remove the Experimental attribute,
  • update docs/usages,
  • add a changelog entry and brief migration note?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think with @kylejuliandev efforts to deprecate the OpenFeature.DependencyInjection library and this would be great candidate for 2.10. Feel free to create a new issue and add it to 2.10 milestone 👍


<PropertyGroup>
<TargetFrameworks>netstandard2.0;net8.0;net9.0;net462</TargetFrameworks>
<RootNamespace>OpenFeature.DependencyInjection.Abstractions</RootNamespace>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Should we have these abstractions in their own namespace? Would it be more accessible to have them in say just the OpenFeature namespace?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kylejuliandev Great question - thanks for calling it out.
I’d keep these in their own namespace for now. OpenFeature.DependencyInjection.Abstractions targets provider authors (integrating new providers), not typical app consumers. Keeping it separate avoids cluttering the root OpenFeature namespace and lets us evolve DI-specific types independently.
Of course, I’m open to discussion about this.

Copy link
Member

@askpt askpt Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer the @arttonoyan. This is a very specific Abstraction layer.

I am wondering if we should keep in a OpenFeature.Abstractions instead. This would be a "library" of just OpenFeature related abstractions like "hooks", "provider"... I am happy to keep OpenFeature.DependencyInjection.Abstractions if the rest of the team agrees.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@askpt @kylejuliandev

Proposal
Rename the package to OpenFeature.Providers.DependencyInjection. This name better reflects scope. It is intuitive for provider authors who want to add DI integration.

Follow-up idea
In a separate discussion, consider extracting common provider contracts into OpenFeature.Providers.Abstractions. We would then have two clear packages. OpenFeature.Providers.Abstractions, and OpenFeature.Providers.DependencyInjection.

Why this helps

  • Clear separation. abstractions live in one place, DI glue in another.
  • Thinner provider packages. they depend on Abstractions, optionally on the DI helpers.
  • Cleaner dependency graph. no DI references inside core contracts.
  • Easier discovery on NuGet. consistent OpenFeature.Providers.* naming.

cc: @beeme1mr

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems reasonable to me. What would be the proposed release strategy and impact? I believe the DI package is still marked as experimental, so breaking changes are allowed, but we should try and make it as straightforward for users to migrate. Also, I'd hope that we could remove the experimental badge shortly after making this change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now our main pain point is coupling. The current abstractions package depends on the OpenFeature SDK because it includes the OpenFeatureProvider abstraction. This means any SDK release. even when it has no provider changes. forces us to bump the abstractions package. Then every provider sees a new version and feels compelled to upgrade. even though nothing changed for providers. Providers should only react to provider related changes.

Decoupling fixes this. If we move provider contracts into OpenFeature.Providers.Abstractions. we isolate them from SDK churn. The DI helpers stay in OpenFeature.Providers.DependencyInjection. Result. cleaner dependency graph. fewer unnecessary upgrades. thinner provider packages. and clearer ownership of changes.

I realize this is a significant change, and it needs attentive and careful attention. If the direction sounds reasonable, I can open a PR to rename the DI package, then draft a proposal for the abstractions split.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds reasonable to me. We've been looking into something similar (minus the DI part) in Java. FYI @chrfwow @toddbaert

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great. I’ll proceed and rename the package to OpenFeature.Providers.DependencyInjection in this PR.

var services = new ServiceCollection();
var provider = new NoOpFeatureProvider();
services.AddOptions<OpenFeatureOptions>().Configure(options =>
services.AddOptions<OpenFeatureProviderOptions>().Configure(options =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: it should be fine for this to remain OpenFeatureOptions right? OpenFeatureOptions extends OpenFeatureProviderOptions

options.DefaultNameSelector = provider =>
{
var options = provider.GetRequiredService<IOptions<OpenFeatureOptions>>().Value;
var options = provider.GetRequiredService<IOptions<OpenFeatureProviderOptions>>().Value;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: it should be fine for this to remain OpenFeatureOptions right? OpenFeatureOptions extends OpenFeatureProviderOptions

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kylejuliandev This is a good question. We cannot do that, because when registering a provider we configure OpenFeatureProviderOptions, not OpenFeatureOptions. If we resolve OpenFeatureOptions, the collection of registered providers will be empty.
This is on me. I made OpenFeatureOptions inherit from OpenFeatureProviderOptions, which is confusing, including for me. Your question highlights that design issue and it is totally valid.
I will remove the inheritance and keep these as two independent option types. There is no need for them to be related.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
}

private readonly HashSet<string> _hookNames = [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: I wonder if we can simplify the Hook registration. If instead of relying on HookNames we just inject Hooks like services.AddTransient<Hook>(myHook). And when we retrieve these hooks out to register them in the provider we just read it back out like var hooks = _serviceProvider.GetServices<Hook>();

Would eliminate the need to track hook names in this options file

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems a great suggestion. My concern is if we register two different instance Hooks of the same type. What should happen? Should we allow this behaviour?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went ahead over the weekend and gave it a stab in #609. Seems simple enough. I think if we register two hooks of the same type, then we should still handle them both. Maybe let's take this conversation over there. Happy to close this here as it's not really relevant to the changes

Copy link
Member

@askpt askpt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work! I only added a couple of minor comments.

Please have a chat with @kylejuliandev to try understand the impact on his deprecations efforts. I remember he mentioned a couple of fixes he was working on for the DI and I don't want that effort to get lost during this PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file misses the README file necessary for the package documentation.

}

/// <inheritdoc />
private async Task InitializeProvidersAsync(CancellationToken cancellationToken)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cancellationToken seems to not be used here.

using OpenFeature.DependencyInjection.Abstractions;
using OpenFeature.Providers.Memory;

namespace OpenFeature.Hosting.Providers.Memory;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should move to OpenFeature.Providers.Memory

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. Moving to OpenFeature.Providers.Memory makes sense. I had the same thought, timing just got in the way. One tweak. I would name it InMemory rather than Memory.

}
}

private readonly HashSet<string> _hookNames = [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems a great suggestion. My concern is if we register two different instance Hooks of the same type. What should happen? Should we allow this behaviour?

arttonoyan and others added 3 commits November 5, 2025 23:47
Refactored `OpenFeatureOptions` to `OpenFeatureProviderOptions`
to improve clarity and maintainability. Updated all references
and test cases to use the new class. Removed inheritance of
`OpenFeatureOptions` from `OpenFeatureProviderOptions` and
introduced `_hookNames` for managing hook names.

Replaced `TestOptions` with `TestProviderOptions` in
`OpenFeatureBuilderExtensionsTests`, adding a `SomeFlag`
property for more flexible testing. Renamed and updated
`OpenFeatureOptionsTests` to `OpenFeatureProviderOptionsTests`.

Performed general cleanup, including removing unused
namespaces, ensuring consistent naming conventions, and
aligning method calls with the new class structure.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Minimal OpenFeature.DependencyInjection.Abstractions with Builder API (Hosting retains DI registrations)

4 participants